代码随想录 哈希表 1 两数之和
题目链接:https://leetcode.cn/problems/two-sum/description/
文章讲解:https://programmercarl.com/0001.两数之和.html
题目描述
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
思路
最初的、最直观的想法 (暴力枚举) 💥
- 思路: 拿起第一个数,然后依次和它后面的所有数配对,看看加起来等不等于 target。如果第一个数不行,就拿起第二个数,和它后面的所有数配对... 以此类推。
- 时间: O(N²)。两层循环,N 是数组长度。
- 空间: O(1)。只用了几个变量。
暴力法的瓶颈在于,对于每个 nums[i],我们都要线性扫描剩下的数组来找 target - nums[i] (我们称之为 complement)。
- 核心问题转化: 对于当前的 nums[i],我们如何快速知道 complement 是否存在于数组的其他位置,并且如果存在,它的下标是什么?
是不是对味了?这不就是**"需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。"**
目前我们有两类元素,一个是遍历数,另一个是 target-遍历数。
本题,就需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是 是否出现在这个集合。
因为题目要求 返回数组下标,因此除了记录元素,还要记录元素下标。可以使用 map 合适。
选项 A: 使用集合 (Set) -
std::unordered_set
- 思路: 遍历数组,对于每个
nums[i]
,计算complement = target - nums[i]
。然后去set
里查complement
是否存在。 - 过程:
- 创建一个空的
unordered_set<int> visited_numbers;
for i from 0 to nums.length - 1:
complement = target - nums[i];
if visited_numbers.count(complement):
- 找到了!
nums[i]
和complement
是我们要找的两个数。 - 问题来了:
complement
的下标是什么?set
只告诉我们它存在,不告诉我们它在原数组的哪个位置。😭
- 找到了!
visited_numbers.insert(nums[i]);
- 创建一个空的
- 复杂度:
- 时间: O(N)。平均情况下,
set
的插入和查找是 O(1)。 - 空间: O(N)。
set
最多可能存储 N 个元素。
- 时间: O(N)。平均情况下,
- 评估: 速度很快!但解决不了"返回下标"的核心需求。此路不通。
- 思路: 遍历数组,对于每个
选项 B: 使用映射 (Map / Dictionary) -
std::unordered_map
启发:
Set
的问题是只存了值,没存下标。Map
可以存键值对 (key-value pair)。我们是不是可以用它来把 值和它的下标关联起来?思路 1 (先填充 Map,再查找):
- 创建一个
unordered_map<int, int> num_to_index;
- 遍历一遍数组,把所有
(nums[k], k)
存入num_to_index
。 - 再遍历一遍数组,对于每个
nums[i]
:complement = target - nums[i];
if num_to_index.count(complement) AND num_to_index[complement] != i:
(确保不是自身)return [i, num_to_index[complement]];
- 复杂度: 两次遍历,时间 O(N) + O(N) = O(N)。空间 O(N)。
- 评估: 可行!比排序好。但能不能一次遍历就搞定?
- 创建一个
思路 2 (边遍历边查找边插入 - 当前代码的思路) 🎉
- 创建一个空的
unordered_map<int, int> record;
(这里的record
扮演的角色是"已经遇到的数字及其下标") for i from 0 to nums.length - 1:
current_num = nums[i];
complement = target - current_num;
- 关键一步: 在
record
中查找complement
。if record.count(complement):
- 这意味着
complement
这个数字我们之前已经遇到过了,并且它的下标被记录在record[complement]
。 - 而
current_num
是我们当前遇到的数。这两个数加起来正好是target
。 - 并且,因为我们是先查后放,
record[complement]
对应的原始元素一定是在nums[i]
之前出现的,所以它们的下标record[complement]
和i
必然不同。 return [record[complement], i];
(或者[i, record[complement]]
,顺序不重要)
- 这意味着
else:
(没在record
中找到complement
)- 说明到目前为止,
current_num
的"另一半"还没出现。 - 那么,把
current_num
和它的下标i
存入record
,供后续的数字来配对。 record[current_num] = i;
- 说明到目前为止,
- 复杂度:
- 时间: O(N)。一次遍历,每次
map
操作平均 O(1)。 - 空间: O(N)。
map
最多存储 N-1 个元素(最坏情况是最后一个元素才和第一个元素配对)。
- 时间: O(N)。一次遍历,每次
- 评估: 非常好!一次遍历,时间空间都满足要求。
- 创建一个空的
为什么是 unordered_map
而不是 map
?
std::map
(基于红黑树):- 优点: 元素按键有序存储。
- 缺点: 查找、插入、删除都是 O(log N)。
std::unordered_map
(基于哈希表):- 优点: 平均情况下,查找、插入、删除都是 O(1)。
- 缺点: 元素无序。哈希冲突极端情况下可能退化到 O(N),但良好设计的哈希函数下很少见。
对于这道题:
- 我们关心的是快速查找
complement
是否存在以及获取其下标。 - 我们完全不需要元素保持任何特定的顺序。
因此,std::unordered_map
的 O(1) 平均时间复杂度更适合我们的需求,性能更优。